import abc
import datetime
import pytz
import tornado.gen

from coin.exchange.base.kr_rest.product import NativeProduct
from coin.exchange.base.kr_rest.futures_product import NativeFuturesProduct
from coin.proto.coin_query_pb2 import (AccountBalance,
                                       AccountPosition,
                                       ProductFillBundle,
                                       ProductOrderBundle)


class PrivateClientAssert(object):
  @staticmethod
  def _assert_account_balance(update):
    assert isinstance(update.msg, AccountBalance)
    assert update.msg.HasField('exchange')
    for balance in update.msg.each_balance:
      assert balance.HasField('currency')
      assert balance.HasField('total')
    return update

  @staticmethod
  def _assert_account_position(update):
    assert isinstance(update.msg, AccountPosition)
    assert update.msg.HasField('exchange')
    for position in update.msg.each_position:
      assert position.HasField('symbol')
      assert position.HasField('net_position')
    for margin in update.msg.each_margin_usage:
      assert margin.HasField('currency')
      assert margin.HasField('total_margin')
    return update

  @staticmethod
  def _assert_account_fill(update):
    assert isinstance(update.msg, ProductFillBundle)
    assert update.msg.HasField('exchange')
    for fill in update.msg.each_fill:
      assert fill.HasField('symbol')
    return update

  @staticmethod
  def _assert_account_order(update):
    assert isinstance(update.msg, ProductOrderBundle)
    assert update.msg.HasField('exchange')
    for order in update.msg.each_order:
      assert order.HasField('symbol')
    return update

  @staticmethod
  def _assert_fill(update, product):
    assert isinstance(update.msg, ProductFillBundle)
    assert update.msg.exchange == product.exchange
    assert update.msg.symbol == product.symbol
    for fill in update.msg.each_fill:
      assert fill.symbol == product.symbol
    return update

  @staticmethod
  def _assert_order(update, product):
    assert isinstance(update.msg, ProductOrderBundle)
    assert update.msg.exchange == product.exchange
    assert update.msg.symbol == product.symbol
    for order in update.msg.each_order:
      assert order.symbol == product.symbol
    return update


class PrivateClientBase(metaclass=abc.ABCMeta):
  def __init__(self):
    pass

  @staticmethod
  def _check_product_type(product):
    assert isinstance(product, (NativeProduct, NativeFuturesProduct)), \
        'Invalid product type'

  @tornado.gen.coroutine
  def async_query_account_balance(self):
    update = yield self.async_query_account_balance_impl()
    PrivateClientAssert._assert_account_balance(update)
    if not update.msg.HasField('query_ts'):
      update.msg.query_ts = int(update.req_timestamp * 10**9)
    return update

  def query_account_balance(self):
    update = self.query_account_balance_impl()
    PrivateClientAssert._assert_account_balance(update)
    if not update.msg.HasField('query_ts'):
      update.msg.query_ts = int(update.req_timestamp * 10 ** 9)
    return update

  def query_account_position(self):
    update = self.query_account_position_impl()
    PrivateClientAssert._assert_account_position(update)
    if not update.msg.HasField('query_ts'):
      update.msg.query_ts = int(update.req_timestamp * 10**9)
    return update

  def query_account_fills(self):
    update = self.query_account_fills_impl()
    PrivateClientAssert._assert_account_fill(update)
    return update

  def query_account_list_orders(self):
    update = self.query_account_list_orders_impl()
    PrivateClientAssert._assert_account_order(update)
    return update

  def query_fills(self, product):
    self._check_product_type(product)
    update = self.query_fills_impl(product)
    PrivateClientAssert._assert_fill(update, product)
    return update

  def query_list_orders(self, product):
    self._check_product_type(product)
    update = self.query_list_orders_impl(product)
    PrivateClientAssert._assert_order(update, product)
    return update

  def query_liquidity_pool_submit(self, product, **kwargs):
    self._check_product_type(product)
    update = self.query_liquidity_pool_submit_impl(product, **kwargs)
    return update

  def query_submit(self, product, side, price, qty):
    self._check_product_type(product)
    update = self.query_submit_impl(product, side, price, qty)
    # assert isinstance(update.msg, None)
    return update

  def query_cancel(self, product, order_id):
    self._check_product_type(product)
    update = self.query_cancel_impl(product, order_id)
    # assert isinstance(update.msg, None)
    return update

  def query_cancel_product(self, product):
    self._check_product_type(product)
    update = self.query_cancel_product_impl(product)
    # assert isinstance(update.msg, None)
    return update

  def query_account_transfer(self, currency):
    update = self.query_account_transfer_impl(currency)
    return update

  def query_history_fills(self, product, start_time, end_time):
    assert isinstance(start_time, datetime.datetime), start_time
    assert isinstance(end_time, datetime.datetime), end_time
    if start_time.tzinfo is None:
      start_time = start_time.replace(tzinfo=pytz.UTC)
    if end_time.tzinfo is None:
      end_time = end_time.replace(tzinfo=pytz.UTC)
    update = self.query_history_fills_impl(product, start_time, end_time)
    return update

  @abc.abstractmethod
  def query_account_balance_impl(self):
    raise NotImplementedError()

  def async_query_account_balance_impl(self):
    raise NotImplementedError()

  # TODO: implement in all subclass and make as abstractmethod
  # @abc.abstractmethod
  def query_account_position_impl(self):
    raise NotImplementedError()

  # TODO: implement in all subclass and make as abstractmethod
  # @abc.abstractmethod
  def query_account_fills_impl(self):
    raise NotImplementedError()

  # TODO: implement in all subclass and make as abstractmethod
  # @abc.abstractmethod
  def query_account_list_orders_impl(self):
    raise NotImplementedError()

  @abc.abstractmethod
  def query_fills_impl(self, product):
    raise NotImplementedError()

  # @abc.abstractmethod
  def query_list_orders_impl(self, product):
    raise NotImplementedError()

  @abc.abstractmethod
  def query_submit_impl(self, product, side, price, qty):
    raise NotImplementedError()

  # @abc.abstractmethod
  def query_cancel_impl(self, product, order_id):
    raise NotImplementedError()

  @abc.abstractmethod
  def query_cancel_product_impl(self, product):
    raise NotImplementedError()

  def query_account_transfer_impl(self, currency):
    raise NotImplementedError()

  def query_history_fills_impl(self, product, start_time, end_time):
    raise NotImplementedError()

class ExWalletPrivateClientBase(PrivateClientBase):
  # only this function shall be implemented.
  # def query_account_balance_impl(self):
  def query_account_position_impl(self):
    raise ValueError("should not be called")

  def query_account_fills_impl(self):
    raise ValueError("should not be called")

  def query_account_list_orders_impl(self):
    raise ValueError("should not be called")

  def query_fills_impl(self, product):
    raise ValueError("should not be called")

  def query_list_orders_impl(self, product):
    raise ValueError("should not be called")

  def query_submit_impl(self, product, side, price, qty):
    raise ValueError("should not be called")

  def query_cancel_impl(self, product, order_id):
    raise ValueError("should not be called")

  def query_cancel_product_impl(self, product):
    raise ValueError("should not be called")
